package operations

import (
	"fmt"
	"github.com/sirupsen/logrus"
	"io"
	"mydocker/common"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"syscall"
)

// RunContainerInitProcess 启动容器的 init 进程
/*
这里的 init 函数是在容器内部执行的，也就是说，代码执行到这里后，容器所在的进程其实就已经创建出来了。
这是本容器的第一个进程。
使用 mount 先去挂载 proc 文件系统，以便后面通过 ps 等系统命令去查看当前进程资源的情况。
*/
func RunContainerInitProcess() error {
	// 根据参数获取命令的完整性，此时不需要再输入完整命令了
	// 从 pipe 中读取命令
	cmdArray := readUserCommand()
	if cmdArray == nil || len(cmdArray) == 0 {
		return fmt.Errorf("run container get user command error, cmdArray is nil")
	}
	// 挂载
	err := setUpMount()
	if err != nil {
		logrus.Errorf("set up mount, err:%v", err)
		return err
	}

	// 在系统环境 PATH 中寻找命令的绝对路径
	path, err := exec.LookPath(cmdArray[0])
	if err != nil {
		logrus.Errorf("look %s path, err:%v", cmdArray[0], err)
		return err
	}
	logrus.Infof("Find path: %s", path)

	// 使用找到的路径和环境变量执行命令，它会覆盖当前进程的镜像、数据和堆栈信息，包括 PID，这些都会被将要运行的进程覆盖掉。
	// 即调用这个方法，将用户指定的程序运行起来，把最初的 init 进程给替换掉，这样当进入到容器内部的时候，就会发现容器内的第一个程序就是我们指定的进程了。
	err = syscall.Exec(path, cmdArray[0:], os.Environ())
	if err != nil {
		return err
	}
	return nil
}

func readUserCommand() []string {
	// uintptr(3) 就是指 index 为 3 的文件描述符，也就是传递进来的管道的另一端，至于为什么是 3，解释如下：
	/*
		因为每个进程默认都会有 3 个文件描述符，分别是标准输入、标准输出、标准错误。这 3 个是子进程一创建的时候就会默认带着的。
		前面通过 ExtraFiles 方式带过来的 readPipe 理所当然地就成为了第 4 个。
		在进程中可以通过 index 方式来读取对应的文件，比如
		index0：标准输入
		index1：标准输出
		index2：标准错误
		index3：带过来的第一个FD(文件描述符)，也就是 readPipe
		由于可以带多个 FD 过来，所以这里的 3 就不是固定的了。
		比如像这样：cmd.ExtraFiles = []*os.File{a,b,c,readPipe} 这里带了4个文件过来，分别的 index 就是 3,4,5,6
		那么我们的 readPipe 就是 index6，读取时就要像这样：pipe := os.NewFile(uintptr(6), "pipe")
	*/
	pipe := os.NewFile(uintptr(3), "pipe")
	defer pipe.Close()
	msg, err := io.ReadAll(pipe)
	if err != nil {
		logrus.Errorf("read piep, err: %v", err)
		return nil
	}
	msgStr := string(msg)
	return strings.Split(msgStr, " ")
}

// setUpMount 设置容器的挂载点
func setUpMount() error {
	pwd, err := os.Getwd() // 获取当前工作目录的路径
	if err != nil {
		logrus.Errorf("Get current location error: %v", err)
		return err
	}
	logrus.Infof("Current location is: %s", pwd)

	// systemd 加入 linux 之后，mount namespace 就变成 shared by default，所以你必须显示声明你要这个新的 mount namespace 独立
	// 如果不先做 private mount，会导致挂载事件外泄，后续执行 pivotRoot 会出现 invalid argument 错误
	err = syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, "")
	if err != nil {
		return err
	}

	// 将当前进程的文件系统视图与原始的文件系统隔离开来
	err = pivotRoot(pwd)
	if err != nil {
		logrus.Errorf("pivotRoot failed, deatil: %v", err)
		return err
	}

	// mount proc（挂载 proc 文件系统，使用特定的标志提高安全性）
	defaultMountFlags := syscall.MS_NOEXEC | syscall.MS_NOSUID | syscall.MS_NODEV
	err = syscall.Mount("proc", "/proc", "proc", uintptr(defaultMountFlags), "")
	if err != nil {
		logrus.Errorf("mount proc, err: %v", err)
		return err
	}
	// 由于前面 pivotRoot 切换了 rootfs，因此这里重新 mount 一下 /dev 目录
	// tmpfs 是基于 件系 使用 RAM、swap 分区来存储。
	// 不挂载 /dev，会导致容器内部无法访问和使用许多设备，这可能导致系统无法正常工作。
	syscall.Mount("tmpfs", "/dev", "tmpfs", syscall.MS_NOSUID|syscall.MS_STRICTATIME, "mode=755")
	return nil
}

// pivotRoot 实现一个“根文件系统”的切换，使得当前进程的文件系统视图与原始的系统文件系统隔离开来
func pivotRoot(root string) error {
	/*
		NOTE：PivotRoot 调用有限制，newRoot 和 oldRoot 不能在同一个文件系统下。
		因此，为了使当前 root 的老 root 和新 root 不在同一个文件系统下，这里把 root 重新 mount 了一次。
		bind mount 是把相同的内容换一个挂载点的挂载方法。
		syscall.MS_BIND：这个选项用于绑定挂载一个文件或目录到另一个位置。当对一个已经存在的挂载点使用 MS_BIND 选项时，
		它会将该挂载点的挂载状态复制到新的目标位置。这通常用于将一个目录挂载到其子目录中，以实现一种“镜像”效果。
		syscall.MS_REC：这个选项代表“递归”（recursive），当与 MS_BIND 结合使用时，MS_REC 会使得挂载操作递归地应用于指定源目录下的所有内容，
		包括子目录和文件。这意味着不仅仅是指定的目录被绑定挂载，其内部的所有内容也会按照相同的方式处理。
	*/
	if err := syscall.Mount(root, root, "bind", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
		logrus.Errorf("mount rootfs to itself, err: %v", err)
		return err
	}
	// 创建 rootfs/.pivot_root 目录用于存储 old_root
	pivotDir := filepath.Join(root, ".pivot_root")
	if err := os.Mkdir(pivotDir, common.Perm0777); err != nil {
		return err
	}
	// 执行 pivot_root 调用，将系统 rootfs 切换到新的 rootfs，
	// PivotRoot 首先将当前根文件系统(old_root)移动到 root 目录下的 pivotDir 目录中，然后将 root 设置为新的根文件系统
	if err := syscall.PivotRoot(root, pivotDir); err != nil {
		logrus.Errorf("pivotRoot failed, new_root: %v old_put: %v", root, pivotDir)
		return err
	}
	// 更改当前工作目录到指定的路径（根目录）
	if err := syscall.Chdir("/"); err != nil {
		logrus.Errorf("chdir to / failed: %v", err)
		return err
	}
	// 最后再把移过来的 old_root umount 了，即 umount rootfs/.pivot_root
	// 由于当前已经是在 rootfs 下，就不能再用上面的 rootfs/.pivot_root 这个路径，现在直接用 /.pivot_root 这个路径即可
	pivotDir = filepath.Join("/", ".pivot_root")
	// syscall.MNT_DETACH 它会执行一个“延迟卸载”操作，这意味着挂载点将对新的访问不可用，但实际的卸载操作会在挂载点不再忙碌时才执行。
	if err := syscall.Unmount(pivotDir, syscall.MNT_DETACH); err != nil {
		logrus.Errorf("unmount pivot_root dir, err: %s\n", err)
		return err
	}
	// 删除临时文件夹
	return os.Remove(pivotDir)
}
